import { Random, escapeRegExp } from "koishi"
import { PassThrough } from "stream"
import * as what from "whatlang-interpreter"
for (const f of "cmd".split(" ")) if (!what.need_svo.includes(f)) what.need_svo.push(f)

export const name = "-lnnbot-whatapi"

export const inject = ["database", "server"]

export function apply(ctx) {
  /*ctx.server.get("/whatnoter/:id", async r => {
    r.set("Access-Control-Allow-Origin", "*")
    r.set("Content-Type", "text/plain; charset=utf-8")

    const match = r.params.id.match(/^(0|[1-9]\d*)([cd])$/)
    if (!match) {
      r.status = 404
      r.body = "Invalid note specifier"
      return
    }
    const uid = +match[1]
    const type = { c: "public", d: "protected" }[match[2]]
    const [d] = await ctx.database.get("whatnoter", uid, [type])
    if (!d) {
      r.status = 404
      r.body = "Uid not present in noter"
      return
    }
    r.status = 200
    r.body = d[type]
  })*/
  /*ctx.server.get("/whatcommands", async r => {
    r.set("Access-Control-Allow-Origin", "*")
    r.set("Content-Type", "application/json; charset=utf-8")

    r.status = 200
    r.body = JSON.stringify((await ctx.database.get("whatcommands", {}, ["name"])).map(e => e.name))
  })
  ctx.server.get("/whatcommands/:name", async r => {
    r.set("Access-Control-Allow-Origin", "*")
    r.set("Content-Type", "application/json; charset=utf-8")

    const [d] = await ctx.database.get("whatcommands", r.params.name)
    if (!d) {
      r.status = 404
      r.body = JSON.stringify({ error: "Specified command does not exist" })
      return
    }
    r.status = 200
    r.body = JSON.stringify(d)
  })*/

  const whatServerLogger = ctx.logger("-lnnbot-whatserver")
  //let whatServerRequestId = 1
  ctx.server.all("/what([\\s\\S]*)", async r => {
    r.set("Access-Control-Allow-Origin", "*")

    let aid = undefined
    const token = r.request.get("x-lnnbot-whatserver-login-token")
    login: if (token) {
      const [tokenInfo] = await ctx.database.get("token", { token }, ["id", "expiredAt"])
      if (!tokenInfo || tokenInfo.expiredAt <= Date.now()) break login
      if (tokenInfo.id === 0) {
        aid = 0
        break login
      }
      const [binding] = await ctx.database.get("binding", { bid: tokenInfo.id })
      //if (!binding) break login
      aid = binding?.aid ?? tokenInfo.id
    }

    const p = decodeURIComponent(r.path.slice(5))
    const command = p.split("/")[0]
    const arg = command === p ? undefined : p.slice(command.length + 1)
    const methodSuffix = r.method === "GET" ? "" : r.method.toLowerCase()
    const d = await ctx.database.get(
      "whatcommands",
      [
        `server ${command}`,
        `serverpost ${command}`,
        `serverput ${command}`,
        `serverdelete ${command}`,
        `serverpatch ${command}`,
        `serverall ${command}`,
      ],
      ["name", "code"],
    )
    if (!d.length) {
      r.status = 404
      r.body = `WhatServer route ${JSON.stringify(command)} does not exist`
      return
    }
    const c = d.find(d => d.name === `server${methodSuffix} ${command}`) ??
      d.find(d => d.name === `serverall ${command}`)
    if (!c) {
      r.status = 405
      r.body = `WhatServer route ${JSON.stringify(command)} does not support ${r.method} requests`
      return
    }
    const { name, code } = c

    const escapeCharMap = { b: "\b", f: "\f", n: "\n", r: "\r", t: "\t" }
    function formatting(value, options = {}) {
      if (value === Infinity) return "Inf"
      if (value === -Infinity) return "-Inf"
      if (value === undefined) return "undef"

      if (typeof value === "string") {
        const { maxStringLength = 4000 } = options
        let maxLen = maxStringLength
        if (maxLen < value.length && value.codePointAt(maxLen - 1) > 0xffff) maxLen--
        const truncated = value.slice(0, maxLen)
        const lines =
          truncated.length > 50
            ? truncated.match(/[^\n\r\f]*(?:\r?\n|\r|\f)?/g).filter(Boolean)
            : [truncated]
        const escapedLines = lines.map(line => {
          line = line.replaceAll("\\", "\\\\").replaceAll('"', '\\"')
          for (const [key, val] of Object.entries(escapeCharMap))
            line = line.replaceAll(val, "\\" + key)
          return line
        })
        let quoted = '"' + escapedLines.join('"\n  "') + '"'
        if (value.length > maxLen) {
          const restCount = value.length - maxLen
          quoted += `... ${restCount} more char${restCount > 1 ? "s" : ""}`
        }
        return quoted
      }

      if (Array.isArray(value)) {
        const { depth = 4, maxArrayLength = 100, _seen = [] } = options
        if (_seen.includes(value)) return "[...circular]"
        if (depth < 0) return "[...]"
        const contents = value.slice(0, maxArrayLength).map(item =>
          formatting(item, {
            ...options,
            depth: depth - 1,
            _seen: [..._seen, value],
          })
        )
        if (value.length > maxArrayLength) {
          const restCount = value.length - maxArrayLength
          contents.push(`... ${restCount} more item${restCount > 1 ? "s" : ""}`)
        }
        if (contents.some(c => c.includes("\n")))
          return (
            "[\n  " +
            contents.map(c => c.replaceAll("\n", "\n  ")).join(",\n  ") +
            "\n]"
          )
        return "[" + contents.join(", ") + "]"
      }

      return String(value)
    }
    function toString(value) {
      if (typeof value === "string") return value
      return formatting(value)
    }
    function headersArrToObj(pairs) {
      const headers = {}
      for (let [key, value] of pairs) {
        key = toString(key)
        value = toString(value)
        if (Object.hasOwn(headers, key)) headers[key] += ", " + value
        else headers[key] = value
      }
      return headers
    }

    const userId = r.request.headers["x-real-ip"] || r.ip
    const fauxCode = `${arg === undefined ? "" : `"${arg.replace(/(["\\])/g, "\\$1")}" `}"${name.replace(/(["\\])/g, "\\$1")}" cmd@`
    const messageId = /*String(whatServerRequestId++)*/Random.id()
    ctx.emit("whatlang/run", fauxCode, {
      sid: `-lnnbot-whatapi:${r.request.headers["x-forwarded-host"] || r.request.headers.host}`,
      isDirect: true,
      userId,
      messageId,
    })
    whatServerLogger.debug("request", messageId, fauxCode)

    const stream = r.body = new PassThrough()
    r.type = "text/plain"

    const { promise, resolve } = Promise.withResolvers()
    let resolved = false

    const output = []
    let time = Date.now()
    const initTime = time
    const stop = ctx.setInterval(() => time = Date.now(), 100)
    const dead_loop_check = () => {
      const now = Date.now()
      if (now - time > 1000 || now - initTime > 60000) return true
    }
    what.eval_what(
      code,
      [[arg]],
      {
        ...what.default_var_dict,
        you: () => `WhatLang/2024 Environment/WhatServer Brand/LNNBot Platform/-lnnbot-whatapi Id/${r.req.headers["x-forwarded-host"] || r.host}`,
        reqh: Array.from(r.req.rawHeaders.filter((_, i) => i % 2 === 0), (x, i) => [x, r.req.rawHeaders[i * 2 + 1]]),
        reqm: r.method.toLowerCase(),
        //reqbget: async () => await (require("raw-body"))(require("inflation")(r.req), "utf-8"),
        //reqbgetb: async () => await (require("raw-body"))(require("inflation")(r.req)),
        reqb: r.request.body ?
          typeof r.request.body === "object" ?
            Symbol.for("unparsedBody") in r.request.body ?
              r.request.body[Symbol.for("unparsedBody")]
              : Object.entries(r.request.body)
            : r.request.body
          : r.method !== "GET" ?
            [...await (require("raw-body"))(require("inflation")(r.req))]
          : undefined,
        hset: (x, y) => r.set(String(x), String(y)),
        me: () => [r.method + " " + decodeURI(r.url.replaceAll("%25", "%2525")), messageId, userId, userId, aid, "__WHATSERVER__", undefined],
        ou: x => { output.push(Buffer.from(x)) },
        nout: () => { output.pop() },
        nouts: x => { output.splice(-x) },
        send: () => {
          whatServerLogger.debug("send", messageId, { resolved })
          stream.write(output.pop())
          resolved || resolve()
          resolved = true
        },
        sends: x => {
          whatServerLogger.debug("sends", messageId, { resolved })
          for (const chunk of output.splice(-x)) stream.write(chunk)
          resolved || resolve()
          resolved = true
        },
        cat: async x => await ctx.http.get(String(x), { responseType: "text" }),
        ca: async x => [...new Uint8Array(await ctx.http.get(String(x), { responseType: "arraybuffer" }))],
        fetch: async (method, url, headers, data) => {
          const resp = await ctx.http(url, {
            method,
            headers: headersArrToObj(headers),
            data: typeof data === "number" ? String(data) : Array.isArray(data) ? Buffer.from(data) : data,
            responseType: "text",
            validateStatus: () => true,
            redirect: "manual",
          })
          return [resp.status, resp.statusText, [...resp.headers], resp.data]
        },
        fech: async (method, url, headers, data) => {
          const resp = await ctx.http(url, {
            method,
            headers: headersArrToObj(headers),
            data: typeof data === "number" ? String(data) : Array.isArray(data) ? Buffer.from(data) : data,
            responseType: "arraybuffer",
            validateStatus: () => true,
            redirect: "manual",
          })
          return [resp.status, resp.statusText, [...resp.headers], [...new Uint8Array(resp.data)]]
        },
        reesc: x => escapeRegExp(x),
        sleep: async x => await new Promise((res) => ctx.setTimeout(res, x * 1000)),
        notewc: async (x, y) => {
          if (aid === undefined) throw new Error("notewc@: you must authenticate to edit data in WhatNoter")
          await ctx.database.upsert("whatnoter", [{uid: x, public: y}], "uid")
        },
        notewd: async x => {
          if (aid === undefined) throw new Error("notewd@: you must authenticate to edit data in WhatNoter")
          await ctx.database.upsert("whatnoter", [{uid: aid, protected: x}], "uid")
        },
        notewe: async x => {
          if (aid === undefined) throw new Error("notewe@: you must authenticate to edit data in WhatNoter")
          await ctx.database.upsert("whatnoter", [{uid: aid, private: x}], "uid")
        },
        noterc: async x => (await ctx.database.get("whatnoter", {uid: x}, ["public"]))[0]?.public ?? null,
        noterd: async x => (await ctx.database.get("whatnoter", {uid: x}, ["protected"]))[0]?.protected ?? null,
        notere: async () => {
          if (aid === undefined) throw new Error("notere@: you must authenticate to access private data in WhatNoter")
          return (await ctx.database.get("whatnoter", {uid: aid}, ["private"]))[0]?.private ?? null
        },
        cmdset: async (x, y) => {
          if (aid === undefined) throw new Error("cmdset@: you must authenticate to edit WhatCommands")
          await ctx.database.upsert("whatcommands", [{name: y, code: x}], "name")
        },
        cmdall: async () => (await ctx.database.get("whatcommands", {}, ["name"])).map(i => i.name),
        cmdsethelp: async (x, y) => {
          if (aid === undefined) throw new Error("cmdsethelp@: you must authenticate to edit WhatCommands")
          await ctx.database.upsert("whatcommands", [{name: y, help: x}], "name")
        },
        cmdseth: async (x, y) => {
          if (aid === undefined) throw new Error("cmdseth@: you must authenticate to edit WhatCommands")
          await ctx.database.upsert("whatcommands", [{name: y, h: x}], "name")
        },
        cmddel: async x => { await ctx.database.remove("whatcommands", {name: x}) },
        cmdget: async x => (await ctx.database.get("whatcommands", {name: x}, ["code"]))[0]?.code ?? null,
        cmdgethelp: async x => (await ctx.database.get("whatcommands", {name: x}, ["help"]))[0]?.help ?? null,
        cmdgeth: async x => (await ctx.database.get("whatcommands", {name: x}, ["h"]))[0]?.h ?? null,
        cmd: async (x, y, s, v, o) => {
          let temp = (await ctx.database.get("whatcommands", {name: y}, ["code"]))[0]?.code
          if (temp == undefined) throw new Error("command not found")
          return await what.exec_what([...s.slice(0, -1), s.at(-1).concat([x, temp])], v, o) ?? null
        },
        [Symbol.for("whatlang.dead_loop_check")]: dead_loop_check,
      },
      x => output.push(x),
    )
      .then(
        res => {
          whatServerLogger.debug("end", messageId, { resolved })
          for (const chunk of output) stream.write(chunk)
          stream.end()
          if (!resolved) {
            let m
            if (
              Array.isArray(res) &&
              /^[2-5]\d\d$/.test(res[0]) &&
              res.length === 2 &&
              typeof res[1] === "string" && /^.+$/.test(res[1])
            ) {
              r.status = +res[0]
              r.message = res[1]
            } else if (m = /^([2-5]\d\d)(?: (.+))?/.exec(res)) {
              r.status = +m[1]
              if (m?.[2]) r.message = m[2]
            } else {
              r.status = 200
            }
            resolve()
          }
        },
        err => {
          whatServerLogger.debug("error-end", messageId, { resolved }, err)
          for (const chunk of output) stream.write(chunk)
          if (resolved || output.length) stream.write("\n")
          stream.end("\fUNCAUGHT " + err.toString())
          if (!resolved) {
            r.status = 500
            resolve()
          }
        }
      )
      .finally(stop)

    return promise
  })

  ctx.server.get("/microcommands", async r => {
    r.set("Access-Control-Allow-Origin", "*")
    r.set("Content-Type", "application/json; charset=utf-8")

    r.status = 200
    r.body = JSON.stringify((await ctx.database.get("dgck81lnn-microcommands", {}, ["name"])).map(e => e.name))
  })
  ctx.server.get("/microcommands/:name", async r => {
    r.set("Access-Control-Allow-Origin", "*")
    r.set("Content-Type", "text/plain; charset=utf-8")

    const [d] = await ctx.database.get("dgck81lnn-microcommands", r.params.name, ["code"])
    if (!d) {
      r.status = 404
      r.body = "Specified microcommand does not exist"
      return
    }
    r.status = 200
    r.body = d.code
  })

  ctx.server.get("/patrons", async r => {
    r.set("Access-Control-Allow-Origin", "*")
    r.set("Content-Type", "text/plain; charset=utf-8")

    r.status = 200
    r.body = JSON.stringify(Object.fromEntries((await ctx.database.get("lnnbot.patron", {})).map(e => [e.uid, { name: e.name, ctime: e.ctime }])))
  })
}
